// Scroll-view implementation for FLTK, based on fltk::ScrollGroup.
// Copyright 2005 (C) Ralph Thomas

#include <view/scroll.h>
#include <view/fltk/common.h>
#include <view/rectangle.h>
#include <util/resources.h>
#include <fltk/ScrollGroup.h>
#include <fltk/Style.h>
#include <fltk/Color.h>
#include <fltk/Symbol.h>
#include <fltk/events.h>
#include <adobe/eve.hpp>
#include <adobe/eve_evaluate.hpp>
#include <adobe/eve_parser.hpp>

namespace view {
	bool operator==( const fltk::Rectangle& lhs, const fltk::Rectangle& rhs ) {
		if( lhs.x() != rhs.x() ) return false;
		if( lhs.y() != rhs.y() ) return false;
		if( lhs.w() != rhs.w() ) return false;
		if( lhs.h() != rhs.h() ) return false;
		return true;
	}
	//
	/// The ScrollLayout class is actually a ScrollGroup widget and an Eve
	/// layout engine. The lifetime of both the Eve engine and the widget
	/// is always the same, and they have to communicate with each other.
	//
	class ScrollLayout : public fltk::ScrollGroup, public adobe::eve_t {
		bool 		_evaluated;	///< Have we ever called evaluate?
		bool		_entered;	///< We are *not* re-entrant.
		adobe::sheet_t& _sheet;		///< For setting the bounding box into.
		adobe::name_t	_bboxName;	///< Cell to put the bounding box into.
		fltk::Rectangle _lastbbox;	///< Last bounding box, for debouncing.
		bool		_gravity;	///< Gravitate towards the selected child.
		//
		/// Return the position of the lowest x and y.
		///
		/// \param	x	[out]	the lowest x-ordinate of any child.
		/// \param	y	[out]	the lowest y-ordinate of any child.
		//
		void getLowestPos( int& x, int& y ) {
			x = 1000000; y = 1000000;
			for( int i = 0; i < children(); i++ ) {
				fltk::Widget* w( child( i ) );
				if( !w ) continue;
				if( w->x() < x ) x = w->x();
				if( w->y() < y ) y = w->y();
			}
		}
	  public:
		ScrollLayout( adobe::sheet_t& sheet, const adobe::name_t& bbox_name, bool gravity, const adobe::name_t& style ) 
		: fltk::ScrollGroup( 0, 0, 10, 10 ), _evaluated( false ), _entered( false ),
		  _sheet( sheet ), _bboxName( bbox_name ), _gravity( gravity ) {
			box( fltk::DOWN_BOX );
			color( fltk::WHITE );
			if( style == adobe::static_name_t( "horizontal" ) )
				type( fltk::ScrollGroup::HORIZONTAL );
			else if( style == adobe::static_name_t( "vertical" ) )
				type( fltk::ScrollGroup::VERTICAL );
		}
		virtual ~ScrollLayout() {}
		void place( const adobe::point_2d_t& pos, const adobe::extents_t& geo )
		{
			resize( pos.x_m, pos.y_m, geo.width(), geo.height() );
			redraw();
		}
		virtual void evaluate( evaluate_options_t options ) {
			if( _entered ) return;
			_entered = true;
			//
			// This evaluate is special:
			//  - It performs an evaluate and then an adjust.
			//  - It stores the position of this widget's children,
			//    so that they can be scrolled back once relayedout.
			//  - It scrolls children out after evaluate.
			//  - It redraws everything.
			//
			int xoff = 0; int yoff = 0;
			if( _evaluated ) getLowestPos( xoff, yoff );
			adobe::eve_t::evaluate( options );
			adobe::eve_t::adjust( w(), h(), options );
			//
			// Now measure again where all of the children are. This
			// is the "not scrolled" (origin) position. Any offset that
			// we have here did *not* come from scrolling, and should be
			// removed from the scrolling offset.
			//
			if( _evaluated ) {
				int zerox = 0;
				int zeroy = 0;
				getLowestPos( zerox, zeroy );
				xoff -= zerox; yoff -= zeroy;
			}
			//
			// Move all of the children back.
			//
			for( int i = 0; i < children(); i++ ) {
				fltk::Widget* w( child( i ) );
				if( !w ) continue;
				w->x( w->x() + xoff );
				w->y( w->y() + yoff );
			}
			//
			// Redraw everything.
			//
			redraw();
			//
			// We have now evaluated once; the child widgets are in
			// places determined by Eve.
			//
			_evaluated = true;
			_entered = false;
		}
		virtual void scrollTo( int x, int y ) {
			fltk::ScrollGroup::scrollTo( x, y );
			//
			// If the bounding box has changed then put the new value
			// into the sheet.
			//
			if( !_bboxName || _entered ) return;
			fltk::Rectangle bbox_now;
			bbox( bbox_now );
			bbox_now.x( x );
			bbox_now.y( y );
			if( bbox_now == _lastbbox ) return;
		 	_lastbbox = bbox_now;
			//
			// Put this result into a generic rectangle and push that into
			// the sheet.
			//
			rectangle r;
			r.x = bbox_now.x(); r.y = bbox_now.y();
			r.w = bbox_now.w(); r.h = bbox_now.h();
			_sheet.set( _bboxName, adobe::value_t( r ) );
			_sheet.update();
		}
		virtual void layout() {
			fltk::ScrollGroup::layout();
			scrollTo( xposition(), yposition() );
			evaluate( evaluate_nested );
			layout_damage( 0 );
		}
		virtual int handle( int code ) {
			if( code != fltk::FOCUS_CHANGE ) return fltk::ScrollGroup::handle( code );
			if( !_gravity ) return fltk::ScrollGroup::handle( code );
			int rval = fltk::ScrollGroup::handle( code );
			//
			// Implement gravity scrolling where we scroll to the currently
			// focussed widget. This code came from the FLTK ScrollGroup
			// implementation.
			//
			fltk::Widget* w( fltk::focus() );
			if( !w ) return rval;
			int x( w->x() ); int y( w->y() );
			//
			// Calculate the absolute offset relative to ourselves.
			//
			for( Group* p = w->parent(); p != this; p = p->parent() ) {
				x += p->x(); y += p->y();
			}
			fltk::Rectangle R; bbox( R );
			int r( x + w->w() );
			int dx( 0 );
			if( x < R.x() ) {
				dx = R.x() - x;
				if( r + dx > R.r() ) {
					dx = R.r() - r;
					if( dx < 0 ) dx = 0;
				}
			}
			int b( y + w->h() );
			int dy = 0;
			if( y < R.y() ) {
				dy = R.y() - y;
				if( b + dy > R.b() ) {
					dy = R.b() - b;
					if( dy < 0 ) dy = 0;
				}
			} else if( b > R.b() ) {
				dy = R.b() - b;
				if( y + dy < R.y() ) {
					dy = R.y() - y;
					if( dy > 0 ) dy = 0;
				}
			}
			scrollTo( xposition() - dx, yposition() - dy );
			//
			// Need to figure out what's going on here; apparently we can't handle
			// moving and changing focus all at once.
			//
			redraw();
			return rval;
		}
	};
	adobe::widget_node_t scroll_factory( const adobe::dictionary_t& parameters,
		const adobe::widget_node_t& parent,
		const adobe::factory_token_t& token,
		factory* f ) {
		//
		// See if we can get the name to push the bounding box into.
		//
		adobe::name_t bboxName;
		parameters.get( adobe::static_name_t( "virtual_box" ), bboxName );
		//
		// Do we want gravity for this scrollview?
		//
		bool gravity( false );
		parameters.get( adobe::static_name_t( "gravity" ), gravity );
		//
		// Which scrollbars do we want to show?
		//
		adobe::name_t style( adobe::static_name_t( "both" ) );
		parameters.get( adobe::static_name_t( "scrollbars" ), style );
		//
		// Create the scrollview widget.
		//
		ScrollLayout* sl( new ScrollLayout( token.sheet_m, bboxName, gravity, style ) );
		fltk::Widget* sg( sl );
		//
		// Insert this widget into it's parent on the main display.
		//
		adobe::display_t::position_t display_token( adobe::insert(
			adobe::get_main_display(), parent.display_token_m, sg ) );
		adobe::display_t::position_t overlay_token(
			token.overlay_m.insert( parent.overlay_token_m ) );
		//
		// Create factory token for children.
		//
		factory_token_ptr_t child_token;
		child_token.layout_m = sl;
		//
		// We can register our new token with the factory. That means that
		// any child widgets will get our token.
		//
		f->push( factory::factory_t( child_token, 0 ) );
		//
		// Bind the calculate and place signals for Eve.
		//
		eve_client::place_func_t placer( boost::bind( &ScrollLayout::place,
			sl, _1, _2 ) );
		//
		// Attempt to get a minimum width and height from the parameters
		// but default to 160 otherwise.
		//
		int width = 160; int height = 160;
		parameters.get<int>( adobe::static_name_t( "min_width" ),
			width );
		parameters.get<int>( adobe::static_name_t( "min_height" ),
			height );

		adobe::extents_t extents;
		extents.width() = width; extents.height() = height;
		adobe::eve_t::signal_suite_t signals;
		signals.eve_calculate_proc_m = boost::bind(
			&eve_client::utility_widget_calculate,
			extents, _1 );
		signals.eve_place_proc_m = boost::bind(
			&eve_client::utility_widget_place, placer, _1, _2,
			boost::ref( token.overlay_m ), overlay_token );
		//
		// Register the scrollgroup with the parent's Eve instance.
		//
		adobe::eve_t::iterator eve_token(
			token.layout_m.add_view_element( parent.eve_token_m,
				adobe::eve_t::insert_element_t(
					adobe::eve_t::calculate_data_t(),
					false,
					parameters,
					signals )
				)
			);
		//
		// Delete the things we created when the assemblage gets destroyed.
		//
		token.assemblage_m.delete_on_destruction( sg );
		//
		// Return the scroll group.
		// 
		return adobe::widget_node_t( parent.size_m, adobe::eve_t::iterator(),
			display_token, overlay_token );
	}
};


